프로필 생성 플로우 구현#102
Hidden character warning
Conversation
`My` 피처를 위한 API 및 Implementation 모듈을 신규 생성하고, 메인 하단 탭 내비게이션에 추가하였습니다.
* **feat: My 피처 모듈 추가**
* `:feature:my:api` 및 `:feature:my:impl` 모듈 생성 및 프로젝트 설정(`settings.gradle.kts`, `app/build.gradle.kts`) 반영
* `MyNavKey`: 내비게이션을 위한 직렬화 가능한 데이터 객체 정의
* `MyScreen`, `MyViewModel`, `MyUiState`: 기본적인 UI 컴포넌트 및 상태 관리 로직 구현
* `MyEntryBuilder`: Hilt와 Navigation3를 이용한 화면 진입점 등록 및 의존성 주입 설정
* **feat: 메인 내비게이션에 My 탭 추가**
* `TopLevelNavItem`: 하단 네비게이션 아이템 목록(`MAIN_NAV_ITEMS`)에 `MyNavKey` 추가
* `strings.xml`: 하단 탭 표시를 위한 리소스(`bottom_nav_my`) 추가
`PrezelTextField`의 `innerTextField`와 `placeholder`를 포함하는 `Box` 컴포저블에 `contentAlignment = Alignment.CenterStart` 속성을 추가하여 내부 콘텐츠가 수직 중앙 및 시작 지점에 올바르게 정렬되도록 수정했습니다.
빈 디렉토리 구조를 유지하기 위해 `core:model` 및 `core:domain` 모듈의 테스트 경로에 `.gitkeep` 파일을 추가했습니다.
`core:model` 모듈에 사용자 정보를 관리하기 위한 `User` 데이터 모델을 추가했습니다. * `User`: id, email, nickname, profileImage, isRegistered 필드를 포함하는 사용자 데이터 클래스 정의 * `ProfileImage`: 이미지 URL 및 기본 이미지 여부(`isDefault`)를 포함하는 하위 데이터 클래스 정의
키보드가 올라올 때 레이아웃의 전역 위치를 계산하여 불필요한 공백 없이 IME 패딩을 적용하는 `advancedImePadding` 확장 함수를 추가했습니다. * `onGloballyPositioned`를 통해 루트 좌표계 대비 현재 컴포넌트의 하단 위치를 계산 * 계산된 하단 여백만큼 `consumeWindowInsets`를 적용하여 중복 패딩 방지 * `imePadding()`을 결합하여 키보드 활성화 시 동적인 패딩 조정 지원
`feature:login:impl` 모듈의 `build.gradle.kts`에 `kotlinx-serialization` 플러그인을 추가하였습니다.
`AndroidManifest.xml` 내 `MainActivity` 설정에 `android:windowSoftInputMode="adjustResize"` 속성을 추가하였습니다. 이를 통해 소프트 키보드가 나타날 때 레이아웃이 가려지지 않고 적절히 조정되도록 개선했습니다.
* feat: 프로필 이미지 변경 및 사진 선택 기능 추가 프로필 화면에서 이미지를 변경하거나 기본 이미지로 초기화할 수 있는 기능을 구현했습니다. * `ProfileViewModel`: `OnProfileImageChanged` 인텐트 처리 로직 추가 및 이미지 URL에 따른 `isDefault` 상태 관리 * `ProfileScreen`: `ActivityResultContracts.PickVisualMedia`를 사용하여 시스템 사진 선택기 연동 * `Avatar`: 프로필 이미지 클릭 시 사진 선택기를 띄우거나 이미지를 제거(기본 이미지로 변경)하는 로직 구현 * 프로필 이미지 우측 하단에 상태에 따라 회전하는 추가/삭제 아이콘 버튼 적용 * refactor: 프로필 UI 상태 및 네비게이션 구조 개선 프로필 상태 관리 모델을 보완하고 관련 컴포넌트 구조를 정리했습니다. * `ProfileUiState`: `profileImage` 필드를 추가하고, 기존 `isPrimaryActionEnabled`를 `submitButtonEnabled`로 명칭 변경 * `ProfileNavKey.Edit`: 프로필 편집 시 기본 이미지 여부를 판단하기 위한 `isDefault` 파라미터 추가 * `ProfileEntryBuilder`: 네비게이션 시 전달받은 `profileUrl`과 `isDefault` 정보를 초기 상태에 반영하도록 수정 * `ProfileScreen`: 내부 레이아웃 로직을 `ProfileScreenContent`로 분리하여 가독성 개선 및 `advancedImePadding` 적용 * `PrezelTextField`: 키보드 타입을 `Text`로 명시하고 불필요한 공백 제거 로직 유지 * style: 문자열 리소스 추가 및 정리 * `feature_profile_impl_profile_image_content_description` 리소스 추가 * `strings.xml` 내 리소스 간격 조정 및 정렬
`User` 데이터 모델 내 `nickname` 프로퍼티의 타입을 기존 커스텀 클래스인 `Nickname`에서 기본 타입인 `String`으로 변경하였습니다.
* refactor: PrezelAsyncImage 이미지 로드 실패 시 에러 로깅 로직 추가 이미지 로드 중 에러가 발생할 경우, 기존 `onError` 콜백 호출과 더불어 `Timber.e`를 통해 예외 상황을 로그로 기록하도록 개선하였습니다. * chore: core:designsystem 모듈에 Timber 의존성 추가 로깅 라이브러리 사용을 위해 `build.gradle.kts`에 `timber` 의존성을 추가하였습니다.
* build: Feature 구현 모듈 공통 의존성에 Timber 추가 `AndroidFeatureImplConventionPlugin`에 `timber` 라이브러리 의존성을 추가하여, 해당 플러그인을 사용하는 모든 Feature 구현 모듈에서 로깅 라이브러리를 사용할 수 있도록 개선했습니다.
유저 정보 조회를 위한 UseCase를 추가하고, 캐싱 로직이 포함된 Repository 구현체를 업데이트했습니다.
* **refactor: ProfileViewModel 내 프로필 제출 활성화 조건 변경**
* `submitProfile` 함수에서 버튼 활성화 여부를 단순히 닉네임 유효성 검사 결과(`NicknameValidationState.Available`)만 체크하던 방식에서, 상태 객체의 `submitButtonEnabled` 프로퍼티를 확인하도록 수정했습니다.
* 프로필 수정 API 호출을 위한 주석 처리된 가이드 코드를 추가했습니다.
* **feat: ProfileUiEffect 내 뒤로가기 액션 추가 및 처리**
* `ProfileUiEffect` 인터페이스에 `OnBack` 오브젝트를 추가했습니다.
* `ProfileScreen`에서 `OnBack` 이펙트 수신 시 `onBack()` 콜백을 호출하도록 처리 로직을 추가했습니다.
`ProfileScreen`의 가독성과 유지보수성을 높이기 위해 내부 UI 로직을 별도 컴포저블로 분리하고 코드 구조를 정리했습니다.
`checkNicknameDuplication` 메서드 내의 `TODO` 주석을 `todo`로 변경하였습니다.
`ProfileUiState`의 상태 판단 로직을 구체화하고, 인터페이스 상속 구조를 정리하여 코드의 일관성을 높였습니다.
닉네임 입력 상태에 따른 검증 상태(NicknameValidationState) 할당 로직을 세분화하고 불필요한 체크를 제거했습니다. * `onNicknameChanged`: 닉네임이 비어있는 경우, 기존 닉네임 유무에 따라 `TooShort` 또는 `Unchecked` 상태를 할당하도록 수정했습니다. * `validateNickname`: 메서드 진입 시 닉네임 빈 값 체크를 단순화하고 중복된 상태 업데이트 로직을 제거했습니다.
|
Caution Review failedPull request was closed or merged during review 📝 WalkthroughWalkthrough새 기능 모듈 My 추가, 프로필 생성/편집 화면 전면 구현, 도메인·데이터 계층(모델·닉네임 검증·레포지토리·유스케이스) 추가, 로그인 네비게이션 분기 변경, 디자인시스템·빌드 설정 및 의존성/네비게이션 맵 업데이트를 도입했습니다. Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (4)
Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAvatar.kt (1)
33-33: 디자인시스템 기본 크기 변경은 호출부 명시로 분리하는 편이 안전합니다.기본값을
REGULAR로 바꾸면 size를 넘기지 않는 모든 호출이 120dp로 바뀝니다. 특정 화면 의도라면 해당 호출부에서만size = PrezelAvatarSize.REGULAR를 명시하고, 컴포넌트 기본값은 유지하는 구성이 회귀를 줄입니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAvatar.kt` at line 33, The component default was changed to PrezelAvatarSize.REGULAR which will silently enlarge every caller that omitted size; revert the parameter default in the PrezelAvatar constructor signature from PrezelAvatarSize.REGULAR back to the original smaller default (e.g. PrezelAvatarSize.SMALL) so existing callers keep their intended size, and for screens that need the larger 120dp explicitly update the call sites to pass size = PrezelAvatarSize.REGULAR; update references to PrezelAvatar and PrezelAvatarSize accordingly.Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.kt (1)
16-32: Create/Edit 엔트리 본문 중복은 공통화해두는 편이 안전합니다.Line 16-32에서
ProfileScreen호출 블록이 동일하므로 공통 함수(또는 공통 람다)로 추출하면 향후 한쪽만 수정되는 드리프트를 줄일 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.kt` around lines 16 - 32, Extract the duplicated ProfileScreen construction used in entry<ProfileNavKey.Create> and entry<ProfileNavKey.Edit> into a single reusable function or lambda (e.g., a private function that takes the LocalNavigator or returns the composable content) and call that from both entries; move the LocalNavigator.current retrieval into the shared function or accept it as a parameter so navigateToHome and onBack closures are created once and reused by both entry<ProfileNavKey.Create> and entry<ProfileNavKey.Edit> (keep symbols ProfileScreen, LocalNavigator.current, navigateToHome, onBack and the two entry<> declarations as the call sites).Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt (2)
99-116: Loading 상태에 대한 UI 처리 부재
uiState가Loading일 때fetchedState는null이 되어 모든 값이 기본값(빈 문자열,true등)으로 처리됩니다. 로딩 중임을 사용자에게 표시하는 UI(스피너, 스켈레톤 등)가 있으면 UX가 개선될 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt` around lines 99 - 116, 현재 ProfileScreen에서 uiState가 Loading일 때 fetchedState가 null로 처리되어 빈 값이 화면에 렌더링됩니다; ProfileScreen에서 uiState (ProfileUiState) 를 분기해 ProfileUiState.Loading일 경우 스피너 또는 스켈레톤을 렌더하도록 추가하고, 기존 ProfileScreenContent 호출은 ProfileUiState.Fetched인 경우에만 fetchedState 값을 전달하도록 변경하세요; 관련 심볼: ProfileScreen, ProfileUiState, fetchedState, ProfileScreenContent, ProfileScreenTopAppBar — Loading 분기에서 적절한 로딩 컴포저블을 표시하거나 ProfileScreenContent에 isLoading 플래그를 전달해 내부에서 로딩 UI를 보여주게 구현하면 됩니다.
53-69: Effect 수집과 Intent 전송 분리 고려현재
LaunchedEffect(Unit)내에서FetchDataIntent 전송과uiEffect수집을 함께 처리하고 있습니다. 이 패턴은 동작하지만, 두 관심사를 분리하면 가독성이 향상됩니다.♻️ 제안하는 리팩토링
LaunchedEffect(Unit) { viewModel.onIntent(ProfileUiIntent.FetchData) + } + LaunchedEffect(Unit) { viewModel.uiEffect.collect { effect -> when (effect) { ProfileUiEffect.NavigateToHome -> navigateToHome() ProfileUiEffect.OnBack -> onBack() is ProfileUiEffect.ShowMessage -> { val resId = when (effect.message) { ProfileUiMessage.CHECK_NICKNAME_FAILED -> R.string.feature_profile_impl_check_nickname_failed_message ProfileUiMessage.FETCH_USER_INFO_FAILED -> R.string.feature_profile_impl_fetch_user_info_failed_message } snackbarHostState.showPrezelSnackbar(message = resources.getString(resId)) } } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt` around lines 53 - 69, Split the current single LaunchedEffect into two focused effects: one LaunchedEffect(Unit) that only dispatches viewModel.onIntent(ProfileUiIntent.FetchData), and a separate LaunchedEffect(Unit) (or LaunchedEffect(viewModel) as appropriate) that collects viewModel.uiEffect.collect and handles ProfileUiEffect cases (NavigateToHome, OnBack, ShowMessage -> resolve ProfileUiMessage to resId and call snackbarHostState.showPrezelSnackbar). This separates intent sending from effect collection (improves readability) while keeping the same handlers (ProfileUiEffect, ProfileUiMessage, snackbarHostState.showPrezelSnackbar, navigateToHome, onBack).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Prezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.kt`:
- Line 28: Remove the Timber dependency line in
AndroidFeatureImplConventionPlugin.kt (the
"implementation"(libs.findLibrary("timber").get()) entry) from the shared
convention and instead add an implementation dependency for Timber in the
profile feature's build.gradle.kts (the profile module's implementation
configuration). This keeps Timber only in the ProfileImpl module and prevents
other feature-impl modules (history, home, login, my, splash) from getting an
unnecessary dependency.
In `@Prezel/core/ui/src/main/java/com/team/prezel/core/ui/AdvancedImePadding.kt`:
- Around line 21-22: The compute for consumePadding in AdvancedImePadding.kt is
wrong: replace coordinates.positionInWindow() with coordinates.positionInRoot()
to use root-relative coordinates, and apply coerceAtLeast(0) to the entire
subtraction result (i.e., (rootSize.height - (positionInRoot().y +
size.height)).toInt().coerceAtLeast(0)) so negative paddings are clamped; update
the identical calculation at the other occurrence (the second consumePadding
usage) to the same pattern referencing findRootCoordinates(), positionInRoot(),
size, and coerceAtLeast.
In
`@Prezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.kt`:
- Around line 58-63: In fetchMyInfo() the local flag isProfileCreateComplete is
hardcoded true which forces NavigateToHome; change this to use the actual user
registration/profile-completion value (or at minimum default to false) instead
of true so new users can reach the profile creation flow—update the logic inside
LoginViewModel.fetchMyInfo() to read the real completion state (or set
isProfileCreateComplete = false) and then call
sendEffect(LoginUiEffect.NavigateToHome) or
sendEffect(LoginUiEffect.NavigateToTerms) based on that value.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.kt`:
- Line 16: The hardcoded Text("My") in MyScreen should be replaced with a string
resource; change the Text call to use stringResource(R.string.my_title) (import
androidx.compose.ui.res.stringResource) so the UI text comes from resources, and
add the key my_title with value "My" to your strings.xml resource file.
In
`@Prezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyUiState.kt`:
- Around line 3-7: MyUiState currently only defines Loading and LoadFailed so it
cannot represent a successful (loaded) state; add a Success variant to the
sealed interface (e.g., data class Success(val data: <payloadType>) : MyUiState)
so MyViewModel/MyScreen can switch on MyUiState for the loaded content instead
of using separate flags; update any code referencing MyUiState to handle
Loading, Success and LoadFailed accordingly and choose an appropriate payload
type name for your feature's data.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.kt`:
- Around line 55-57: The current submitButtonEnabled logic allows submission
when only profileImage changed even if nickname is invalid; change it so that if
nickname was modified (nickname != originalNickname) the button is enabled only
when nicknameValidation == NicknameValidationState.Available, otherwise
(nickname unchanged) enable the button when profileImage !=
originalProfileImage; update the submitButtonEnabled computation (referencing
submitButtonEnabled, nickname, originalNickname, nicknameValidation,
NicknameValidationState.Available, profileImage, originalProfileImage) to
implement this conditional branching.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 120-123: 현재 닉네임 검증 결과가 오래된 입력에 덮어쓰는 문제를 방지하려면 updateState 블록 안에서
fetchedState.updateProfile(...)을 호출하기 전에 검증 대상 닉네임이 현재 UI 상태의 nickname과 일치하는지
확인하도록 변경하세요; 즉, ProfileUiState.Fetched에서 가져온 fetchedState.nickname과
validationState(현재 변수명)의 검증 대상(예: validationState.input 또는
validationState.nickname)에 대해 equality 체크를 하고 다르면 기존 currentState를 반환하여 무시하도록
처리한 뒤, 같을 때만 fetchedState.updateProfile(nicknameValidation = validationState)을
호출하도록 수정하세요.
- Around line 133-150: submitProfile currently always sends
ProfileUiEffect.NavigateToHome even when editing; change it to branch on the
runtime type of fetchedState (ProfileUiState.Create vs ProfileUiState.Edit) and
send the appropriate effect (NavigateToHome for Create, OnBack for Edit) using
sendEffect, and when re-enabling the patchUserProfileUseCase callback ensure the
success handler uses the same branching logic (refer to submitProfile,
ProfileUiState.Fetched, ProfileUiState.Create, ProfileUiState.Edit,
ProfileUiEffect.NavigateToHome, ProfileUiEffect.OnBack, and sendEffect).
---
Nitpick comments:
In
`@Prezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAvatar.kt`:
- Line 33: The component default was changed to PrezelAvatarSize.REGULAR which
will silently enlarge every caller that omitted size; revert the parameter
default in the PrezelAvatar constructor signature from PrezelAvatarSize.REGULAR
back to the original smaller default (e.g. PrezelAvatarSize.SMALL) so existing
callers keep their intended size, and for screens that need the larger 120dp
explicitly update the call sites to pass size = PrezelAvatarSize.REGULAR; update
references to PrezelAvatar and PrezelAvatarSize accordingly.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.kt`:
- Around line 16-32: Extract the duplicated ProfileScreen construction used in
entry<ProfileNavKey.Create> and entry<ProfileNavKey.Edit> into a single reusable
function or lambda (e.g., a private function that takes the LocalNavigator or
returns the composable content) and call that from both entries; move the
LocalNavigator.current retrieval into the shared function or accept it as a
parameter so navigateToHome and onBack closures are created once and reused by
both entry<ProfileNavKey.Create> and entry<ProfileNavKey.Edit> (keep symbols
ProfileScreen, LocalNavigator.current, navigateToHome, onBack and the two
entry<> declarations as the call sites).
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt`:
- Around line 99-116: 현재 ProfileScreen에서 uiState가 Loading일 때 fetchedState가 null로
처리되어 빈 값이 화면에 렌더링됩니다; ProfileScreen에서 uiState (ProfileUiState) 를 분기해
ProfileUiState.Loading일 경우 스피너 또는 스켈레톤을 렌더하도록 추가하고, 기존 ProfileScreenContent 호출은
ProfileUiState.Fetched인 경우에만 fetchedState 값을 전달하도록 변경하세요; 관련 심볼: ProfileScreen,
ProfileUiState, fetchedState, ProfileScreenContent, ProfileScreenTopAppBar —
Loading 분기에서 적절한 로딩 컴포저블을 표시하거나 ProfileScreenContent에 isLoading 플래그를 전달해 내부에서 로딩
UI를 보여주게 구현하면 됩니다.
- Around line 53-69: Split the current single LaunchedEffect into two focused
effects: one LaunchedEffect(Unit) that only dispatches
viewModel.onIntent(ProfileUiIntent.FetchData), and a separate
LaunchedEffect(Unit) (or LaunchedEffect(viewModel) as appropriate) that collects
viewModel.uiEffect.collect and handles ProfileUiEffect cases (NavigateToHome,
OnBack, ShowMessage -> resolve ProfileUiMessage to resId and call
snackbarHostState.showPrezelSnackbar). This separates intent sending from effect
collection (improves readability) while keeping the same handlers
(ProfileUiEffect, ProfileUiMessage, snackbarHostState.showPrezelSnackbar,
navigateToHome, onBack).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 861a1677-897d-4458-a05c-1030c2d238e3
📒 Files selected for processing (58)
Prezel/app/build.gradle.ktsPrezel/app/src/main/AndroidManifest.xmlPrezel/app/src/main/java/com/team/prezel/navigation/TopLevelNavItem.ktPrezel/build-logic/convention/src/main/java/com/team/prezel/buildlogic/convention/plugin/AndroidFeatureImplConventionPlugin.ktPrezel/core/data/build.gradle.ktsPrezel/core/data/src/main/java/com/team/prezel/core/data/di/RepositoryModule.ktPrezel/core/data/src/main/java/com/team/prezel/core/data/repository/UserRepositoryImpl.ktPrezel/core/designsystem/build.gradle.ktsPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAsyncImage.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/PrezelAvatar.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextField.ktPrezel/core/designsystem/src/main/java/com/team/prezel/core/designsystem/component/textfield/PrezelTextFieldState.ktPrezel/core/domain/.gitignorePrezel/core/domain/build.gradle.ktsPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/repository/profile/UserRepository.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/FetchUserInfoUseCase.ktPrezel/core/domain/src/main/kotlin/com/team/prezel/core/domain/usecase/user/ValidateNicknameUseCase.ktPrezel/core/domain/src/test/kotlin/com/team/prezel/core/domain/.gitkeepPrezel/core/model/src/main/java/com/team/prezel/core/model/profile/Nickname.ktPrezel/core/model/src/main/java/com/team/prezel/core/model/profile/User.ktPrezel/core/model/src/test/java/com/team/prezel/core/.gitkeepPrezel/core/ui/src/main/java/com/team/prezel/core/ui/AdvancedImePadding.ktPrezel/feature/login/impl/build.gradle.ktsPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/LoginViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/landing/contract/LoginUiEffect.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/navigation/LoginEntryBuilder.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsScreen.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/TermsViewModel.ktPrezel/feature/login/impl/src/main/java/com/team/prezel/feature/login/impl/terms/contract/TermsUiEffect.ktPrezel/feature/my/api/build.gradle.ktsPrezel/feature/my/api/consumer-rules.proPrezel/feature/my/api/proguard-rules.proPrezel/feature/my/api/src/main/java/com/team/prezel/feature/my/api/MyNavKey.ktPrezel/feature/my/impl/build.gradle.ktsPrezel/feature/my/impl/consumer-rules.proPrezel/feature/my/impl/proguard-rules.proPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyScreen.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyUiState.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/MyViewModel.ktPrezel/feature/my/impl/src/main/java/com/team/prezel/feature/my/impl/navigation/MyEntryBuilder.ktPrezel/feature/my/impl/src/main/res/values/strings.xmlPrezel/feature/profile/api/src/main/java/com/team/prezel/feature/profile/api/ProfileNavKey.ktPrezel/feature/profile/impl/build.gradle.ktsPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/component/NicknameTextField.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/component/ProfileImageEditor.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/component/ProfileScreenTopAppBar.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiEffect.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiIntent.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/ProfileUiMessage.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.ktPrezel/feature/profile/impl/src/main/res/values/strings.xmlPrezel/gradle/libs.versions.tomlPrezel/settings.gradle.kts
💤 Files with no reviewable changes (1)
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileUiState.kt
`advancedImePadding` 수정자 내에서 컴포넌트의 하단 위치를 계산할 때의 기준 좌표와 수치 변환 방식을 수정하였습니다. * `positionInWindow()` 대신 `positionInRoot()`를 사용하여 윈도우 기준이 아닌 루트 레이아웃 기준으로 좌표를 계산하도록 변경했습니다. * 좌표 값 변환 시 `toInt()` 대신 `roundToInt()`를 적용하여 정확도를 개선하고 불필요한 `coerceAtLeast(0)` 호출을 정리했습니다.
`ProfileUiState.Fetched` 상태에서 프로필 수정 버튼(`submitButtonEnabled`)이 활성화되는 조건을 더 엄격하게 개선했습니다. * 닉네임의 유효성 상태(`nicknameValidation`)가 `Available`인 경우를 필수 조건으로 설정하였습니다. * 닉네임 또는 프로필 이미지 중 최소 하나라도 변경되었을 때만 버튼이 활성화되도록 로직을 수정했습니다. (기존에는 닉네임이 유효하지 않은 상태여도 프로필 이미지만 변경되면 버튼이 활성화될 수 있었던 문제를 해결)
`updateNicknameValidation` 함수에서 유효성 검사 결과가 현재 입력된 닉네임과 일치할 때만 상태를 업데이트하도록 방어 로직을 추가했습니다. 비동기 처리 과정에서 발생할 수 있는 상태 불일치 문제를 방지합니다.
`ProfileUiState`의 복잡한 인터페이스 구조를 단순화하고, 관련 도메인 모델의 위치를 재정의하여 가독성과 유지보수성을 높였습니다.
* **refactor: ProfileUiState 구조 단순화 및 통합**
* 기존 `Fetched`, `Create`, `Edit`으로 나뉘어 있던 상태 구조를 `ProfileUiState.Content` 단일 데이터 클래스로 통합했습니다.
* `isNewProfile` 여부를 상태 클래스 내부가 아닌 `ProfileScreen` 파라미터를 통해 외부에서 주입받도록 변경했습니다.
* `canPhotoPickerLaunch()` 함수를 `shouldLaunchPhotoPicker` 프로퍼티로 변경하고 로직을 정리했습니다.
* `User.toUiState()` 확장 함수 내에서 사용자 등록 여부(`isRegistered`)에 따라 초기 닉네임 검증 상태를 설정하도록 수정했습니다.
* **refactor: NicknameValidationState 위치 변경**
* `ProfileUiState.kt` 내부에 정의되어 있던 `NicknameValidationState` enum 클래스를 별도 파일(`model/NicknameValidationState.kt`)로 분리했습니다.
* **refactor: ProfileViewModel 로직 수정**
* 변경된 `ProfileUiState.Content` 구조에 맞춰 `handleNicknameChanged`, `handleProfileImageChanged` 등의 상태 업데이트 로직을 `copy` 기반으로 간소화했습니다.
* **style: ProfileScreen 및 Preview 코드 정리**
* `ProfileScreen` 컴포저블에 `isNewProfile` 파라미터를 추가하여 상단 바 UI(`isCreate`) 결정 로직을 분리했습니다.
* 중복되던 `EditProfileScreenPreview`를 제거하고 통합된 상태를 사용하는 `CreateProfileScreenPreview`로 정리했습니다.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt (1)
134-151:⚠️ Potential issue | 🟠 Major편집 저장 후에도 항상 홈으로 이동합니다.
지금은 생성/편집 구분 없이
NavigateToHome만 보내고 있어서,ProfileNavKey.Edit로 들어온 경우도 저장 후 홈으로 튑니다. 현재ProfileUiState.Content에는 모드 정보가 없으니,isNewProfile를 ViewModel 상태나 submit intent로 함께 넘겨서 effect를 분기해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt` around lines 134 - 151, The submitProfile() always sends ProfileUiEffect.NavigateToHome regardless of create/edit; add a mode flag (e.g., isNewProfile) to the ViewModel state or include it on the submit intent so submitProfile() can branch: inside submitProfile(), read the mode from ProfileUiState.Content (or the submit intent) and send ProfileUiEffect.NavigateToHome when isNewProfile is true, otherwise send ProfileUiEffect.OnBack for edit; update the ProfileUiState.Content (or the submit flow) to carry that isNewProfile boolean and use that symbol in submitProfile() to decide which effect to send.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.kt`:
- Around line 76-85: The current onClickProfileImage handler only launches
photoPickerLauncher when uiState.shouldLaunchPhotoPicker is true and otherwise
calls viewModel.onIntent(ProfileUiIntent.OnProfileImageChanged(profileUrl = ""))
which deletes the image immediately; update this so tapping always opens the
photo picker instead of sending an empty URL (or split delete into a distinct
action), i.e., change the onClickProfileImage logic to always call
photoPickerLauncher.launch(PickVisualMediaRequest(...)) and move deletion to a
separate UI affordance (e.g., a dedicated delete button or long-press) that
dispatches ProfileUiIntent.OnProfileImageChanged(profileUrl = "") via
viewModel.onIntent.
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 53-60: On fetchUserInfo failure you only send an effect but never
update the UI state, leaving the screen stuck in Loading; in the onFailure block
of fetchUserInfoUseCase() call update the ViewModel state (via updateState) to
reflect a non-loading / error or retry-able state (e.g. set isLoading = false
and set an error flag/message or switch to a ProfileUiState.Error) and still
call
sendEffect(ProfileUiEffect.ShowMessage(ProfileUiMessage.FETCH_USER_INFO_FAILED));
update the onFailure branch to both send the effect and mutate the state so the
UI can render retry controls instead of an empty loading screen.
---
Duplicate comments:
In
`@Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.kt`:
- Around line 134-151: The submitProfile() always sends
ProfileUiEffect.NavigateToHome regardless of create/edit; add a mode flag (e.g.,
isNewProfile) to the ViewModel state or include it on the submit intent so
submitProfile() can branch: inside submitProfile(), read the mode from
ProfileUiState.Content (or the submit intent) and send
ProfileUiEffect.NavigateToHome when isNewProfile is true, otherwise send
ProfileUiEffect.OnBack for edit; update the ProfileUiState.Content (or the
submit flow) to carry that isNewProfile boolean and use that symbol in
submitProfile() to decide which effect to send.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: dc5bc693-8687-48e2-9ec7-238613b888d0
📒 Files selected for processing (6)
Prezel/core/ui/src/main/java/com/team/prezel/core/ui/AdvancedImePadding.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileScreen.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/ProfileViewModel.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/contract/ProfileUiState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/NicknameValidationState.ktPrezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.kt
✅ Files skipped from review due to trivial changes (1)
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/model/NicknameValidationState.kt
🚧 Files skipped from review as they are similar to previous changes (2)
- Prezel/feature/profile/impl/src/main/java/com/team/prezel/feature/profile/impl/navigation/ProfileEntryBuilder.kt
- Prezel/core/ui/src/main/java/com/team/prezel/core/ui/AdvancedImePadding.kt
`Nickname.Companion.create(nickname)` 형태의 명시적 `Companion` 참조를 `Nickname.create(nickname)`으로 변경하여 코드를 간결하게 수정하였습니다.
* `feature_profile_impl_nickname_text_field_placeholder` 리소스의 내용을 "닉네임을 입력해 주세요"에서 "최대 10자까지 입력이 가능해요"로 변경하였습니다. * 관련 리소스 영역의 주석을 `Helper Message`에서 `Nickname TextField`로 수정하였습니다.
`ProfileUiIntent`의 명칭을 행위 중심의 직관적인 이름으로 변경하고, 이를 `ProfileViewModel` 및 `ProfileScreen`에 반영하였습니다.
* **refactor: ProfileUiIntent 파라미터 및 클래스 명칭 변경**
* `OnNicknameChanged` -> `UpdateNickname`
* `OnProfileImageChanged` -> `UpdateProfileImage`
* `OnClickSubmit` -> `SubmitProfile`
* **refactor: ViewModel 및 UI 레이어 내 변경 사항 반영**
* `ProfileViewModel`의 `onIntent` 핸들러 내 변경된 Intent 명칭 적용
* `ProfileScreen`에서 `viewModel.onIntent` 호출 시 변경된 Intent 클래스 사용
📌 작업 내용
로그인 이후 온보딩 흐름에 프로필 생성 단계를 연결하고, 프로필 생성/편집 화면 및 관련 도메인 계층을 추가했습니다.
함께
My피처 모듈을 신설해 하단 탭의 프로필 진입점을My로 정리했습니다.로그인/약관 동의 이후 내비게이션 흐름 정리
UiEffect추가ProfileNavKey를Create/Edit로 분리프로필 생성/편집 화면 구현
프로필 관련 도메인/데이터 계층 추가
User,Nickname모델 추가FetchUserInfoUseCase,ValidateNicknameUseCase추가UserRepository구현 및 Hilt 바인딩 추가My피처 모듈 추가feature:my:api,feature:my:impl신설My로 교체UI/인프라 보완
advancedImePadding추가로 키보드 대응 개선PrezelAvatar,PrezelTextField정렬/기본 스타일 보정PrezelAsyncImage에러 로깅 추가core-domain모듈 및 관련 의존성/설정 추가🧩 관련 이슈
📸 스크린샷
Screen_recording_20260416_231829.mp4
📢 참고사항
UserRepositoryImpl의 유저 정보/닉네임 중복 검사는 테스트용 mock 구현입니다.Summary by CodeRabbit
릴리스 노트
New Features
UI/UX 개선
Chores